<template>
  <div class="file-upload">
    <div class="upload-area" :class="{ 'is-dragover': drag && isDragOver }" v-on="events">
      <slot v-if="isUploading" name="loading">
        <button disabled>正在上传</button>
      </slot>
      <slot v-else-if="lastFileData && lastFileData.loaded" :uploaded-data="lastFileData.data" name="uploaded">
        <button>点击上传</button>
      </slot>
      <slot v-else>
        <button>点击上传</button>
      </slot>
    </div>
    <input ref="fileInput" type="file" :style="{ display: 'none' }" @change="handleFileChange" />
    <ul v-if="showUploadList" :class="`upload-list upload-list-${listType}`">
      <li v-for="file in filesList" :key="file.uid" :class="`uploaded-file upload-${file.status}`">
        <img v-if="file.url && listType === 'picture'" class="upload-list-thumbnail" :src="file.url" :alt="file.name" />
        <el-icon v-if="file.status === 'loading'" class="is-loading file-icon"><Loading /></el-icon>
        <el-icon v-else class="file-icon"><Document /></el-icon>
        <span class="filename">{{ file.name }}</span>
        <span class="delete-icon" @click="removeFile(file.uid)">
          <el-icon><Delete /></el-icon>
        </span>
      </li>
    </ul>
  </div>
</template>

<!-- 支持自定义上传界面插槽 -->
<!-- 支持一系列生命周期钩子函数 -->
<!-- beforeUpload onProgress onSuccess onError onChange -->
<script lang="ts" setup>
import { v4 as uuidv4 } from 'uuid'
import { Delete, Loading, Document } from '@element-plus/icons-vue'
import { last } from 'lodash-es'
import { uploadFile, UploadData } from '@/service/request'

type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
type FileListType = 'picture' | 'text'
type CheckUpload = (file: File) => boolean | Promise<File>

interface UploadFile {
  uid: string
  size: number
  name: string
  status: UploadStatus
  raw: File
  resp?: UploadData
  url?: string
}

const props = withDefaults(
  defineProps<{
    action: string
    beforeUpload?: CheckUpload
    drag?: boolean
    autoUpload?: boolean
    listType?: FileListType
    showUploadList?: boolean
  }>(),
  {
    drag: false,
    autoUpload: true,
    listType: 'text',
    showUploadList: true
  }
)
const emit = defineEmits<{
  (e: 'success', data: { res: UploadData; file: UploadFile; fileList: UploadFile[] }): void
  (e: 'change', data: { file: File }): void
  (e: 'error', data: { error: any; file: UploadFile; fileList: UploadFile[] }): void
}>()

const fileInput = ref<null | HTMLInputElement>(null)
const filesList = ref<UploadFile[]>([])
const isDragOver = ref(false)

const isUploading = computed(() => {
  return filesList.value.some(file => file.status === 'loading')
})

const lastFileData = computed(() => {
  const lastFile = last(filesList.value)
  if (lastFile) {
    return {
      loaded: lastFile.status === 'success',
      data: lastFile.resp
    }
  }
  return false
})

const removeFile = (id: string) => {
  filesList.value = filesList.value.filter(file => file.uid === id)
}

const triggerUpload = () => {
  if (fileInput.value) {
    fileInput.value.click()
  }
}

const postFile = async (readyFile: UploadFile) => {
  readyFile.status = 'loading'
  try {
    const rawData = await uploadFile<UploadData>(props.action, readyFile.raw)
    readyFile.status = 'success'
    readyFile.resp = rawData.data
    emit('success', { res: rawData.data, file: readyFile, fileList: filesList.value })
  } catch (error) {
    readyFile.status = 'error'
    emit('error', { error, file: readyFile, fileList: filesList.value })
  } finally {
    if (fileInput.value) {
      fileInput.value.value = ''
    }
  }
}

const addFileToList = (uploadedFile: File) => {
  emit('change', { file: uploadedFile })
  const fileObj = reactive<UploadFile>({
    uid: uuidv4(),
    size: uploadedFile.size,
    name: uploadedFile.name,
    status: 'ready',
    raw: uploadedFile
  })
  if (props.listType === 'picture') {
    try {
      // 图片预览功能 URL.createObjectURL  FileReader.readAsDataUrl
      // createObjectURL: 将一个file或Blob类型的对象转为UTF-16的字符串，并保存在当前操作的document下；需要注意的是使用 revokeObjectURL()手动清除
      // 扩展：UTF-8与UTF-16与GBK到底有啥区别， 都是可变长度的编码方式 通过对Unicode码值进行对应规则转换后，编码保持到内存/文件中
      fileObj.url = URL.createObjectURL(uploadedFile)
    } catch (err) {
      console.error('上传文件失败', err)
    }
  }
  filesList.value.push(fileObj)
  // emit('localSuccess', uploadedFile)
  if (props.autoUpload) {
    postFile(fileObj)
  }
}

const beforeUploadCheck = (files: null | FileList) => {
  if (files) {
    const uploadedFile = files[0]
    if (props.beforeUpload) {
      const result = props.beforeUpload(uploadedFile)
      if (result && result instanceof Promise) {
        result
          .then(processedFile => {
            if (processedFile instanceof File) {
              addFileToList(processedFile)
            } else {
              throw new Error('beforeUpload Promise 应该是个文件类型')
            }
          })
          .catch(e => {
            console.error(e)
          })
      } else if (result === true) {
        addFileToList(uploadedFile)
      }
    } else {
      addFileToList(uploadedFile)
    }
  }
}

// 手动上传
const uploadFiles = () => {
  filesList.value.filter(file => file.status === 'ready').forEach(readyFile => postFile(readyFile))
}

let events: { [key: string]: (e: any) => void } = {
  click: triggerUpload
}

const handleFileChange = (e: Event) => {
  const target = e.target as HTMLInputElement
  beforeUploadCheck(target.files)
}

const handleDrag = (e: DragEvent, over: boolean) => {
  e.preventDefault()
  isDragOver.value = over
}

const handleDrop = (e: DragEvent) => {
  e.preventDefault()
  isDragOver.value = false
  if (e.dataTransfer) {
    beforeUploadCheck(e.dataTransfer.files)
  }
}
if (props.drag) {
  events = {
    ...events,
    dragover: (e: DragEvent) => handleDrag(e, true),
    dragleave: (e: DragEvent) => handleDrag(e, false),
    drop: handleDrop
  }
}

defineExpose({
  uploadFiles
})

// 销毁组件之前需要将内存中的URL销毁
onBeforeUnmount(() => {
  filesList.value.forEach(({ url }) => {
    if (url?.startsWith('blob:')) URL.revokeObjectURL(url)
  })
})
</script>
<style lang="scss" scoped>
// .file-upload {
//   .upload-area {
//     background: #efefef;
//     border: 1px dashed #ccc;
//     border-radius: 4px;
//     cursor: pointer;
//     padding: 20px;
//     width: 360px;
//     height: 180px;
//     text-align: center;
//     &:hover {
//       border: 1px dashed #1890ff;
//     }
//     &.is-dragover {
//       border: 2px dashed #1890ff;
//       background: rgba(#1890ff, 0.2);
//     }
//   }
// }
.upload-list {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.upload-list li {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
  font-size: 14px;
  line-height: 1.8;
  margin-top: 5px;
  box-sizing: border-box;
  border-radius: 4px;
  min-width: 200px;
  position: relative;
  &:first-child {
    margin-top: 10px;
  }
  &.uploaded-file {
    display: flex;
    align-items: center;
  }
  .upload-list-thumbnail {
    vertical-align: middle;
    display: inline-block;
    width: 70px;
    height: 70px;
    position: relative;
    z-index: 1;
    background-color: #fff;
    object-fit: cover;
  }
  .file-icon {
    margin-right: 5px;
    color: rgba(0, 0, 0, 0.45);
  }
  .filename {
    margin-left: 5px;
    margin-right: 40px;
  }
  // &.upload-loading {
  //   color: yellow;
  //   svg {
  //     color: yellow;
  //   }
  // }
  &.upload-success {
    color: green;
    svg {
      color: green;
    }
  }
  &.upload-error {
    color: #f5222d;
    svg {
      color: #f5222d;
    }
  }
  .file-status {
    display: block;
    position: absolute;
    right: 5px;
    top: 0;
    line-height: inherit;
  }
  .delete-icon {
    display: none;
    position: absolute;
    right: 7px;
    top: 2px;
    line-height: inherit;
    cursor: pointer;
  }
  &:hover {
    background-color: #efefef;
    .file-status {
      display: none;
    }
    .delete-icon {
      display: block;
    }
  }
}
</style>
